This post has been updated to support the new HttpClientModule .
When using a full-fledged framework like Angular 5 to talk to your server with AccessTokens, you might not use cookies. In this case, any image requests to your server with an <img /> tag will fail if these need to be authorized.
I have coded a little UrlService and a pipe to help you out. You will need to adapt to your services.
The Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { Subscriber } from 'rxjs/Subscriber'; @Injectable() export class UrlHelperService { constructor( private http: HttpClient, /* Your favorite Http requester */ ) { } get(url: string): Observable { return new Observable((observer: Subscriber) => { let objectUrl: string = null; this.http .get(url, { headers: this.getHeaders(), responseType: 'blob' }) .subscribe(m => { objectUrl = URL.createObjectURL(m); observer.next(objectUrl); }); return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl); objectUrl = null; } }; }); } getHeaders(): HttpHeaders { let headers = new HttpHeaders(); ////let token = this.authService.getCurrentToken(); let token = { access_token: 'ABCDEF' }; // Get this from your auth service. if (token) { headers.set('Authorization', 'Bearer ' + token.access_token); } return headers; } } |
This code will allow you to request an image with your access token, then return Observable<string> which will contain a Blob URL. This is using the FileAPI with the method createObjectURL.
Notice also that we return a function for revoking the URL. This way, we avoid memory leaks.
With Angular 5, I would also recommend that you do not hook the getHeaders() token in this service but use an interceptor.
The Pipe
Let’s create a pipe that uses the service above. I have adapted the “async” pipe so you don’t have to write | secure | async all the time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
import { Pipe, PipeTransform, OnDestroy, WrappedValue, ChangeDetectorRef } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import 'rxjs/add/observable/of'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { UrlHelperService } from '../services/url-helper.service'; // Using similarity from AsyncPipe to avoid having to pipe |secure|async in HTML. @Pipe({ name: 'secure', pure: false }) export class SecurePipe implements PipeTransform, OnDestroy { private _latestValue: any = null; private _latestReturnedValue: any = null; private _subscription: Subscription = null; private _obj: Observable = null; private previousUrl: string; private _result: BehaviorSubject = new BehaviorSubject(null); private result: Observable = this._result.asObservable(); private _internalSubscription: Subscription = null; constructor( private _ref: ChangeDetectorRef, private urlHelperService: UrlHelperService, private sanitizer: DomSanitizer ) { } ngOnDestroy(): void { if (this._subscription) { this._dispose(); } } transform(url: string): any { let obj = this.internalTransform(url); return this.asyncTrasnform(obj); } private internalTransform(url: string): Observable { if (!url) { return this.result; } if (this.previousUrl !== url) { this.previousUrl = url; this._internalSubscription = this.urlHelperService.get(url).subscribe(m => { let sanitized = this.sanitizer.bypassSecurityTrustUrl(m); this._result.next(sanitized); }); } return this.result; } private asyncTrasnform(obj: Observable): any { if (!this._obj) { if (obj) { this._subscribe(obj); } this._latestReturnedValue = this._latestValue; return this._latestValue; } if (obj !== this._obj) { this._dispose(); return this.asyncTrasnform(obj); } if (this._latestValue === this._latestReturnedValue) { return this._latestReturnedValue; } this._latestReturnedValue = this._latestValue; return WrappedValue.wrap(this._latestValue); } private _subscribe(obj: Observable) { var _this = this; this._obj = obj; this._subscription = obj.subscribe({ next: function (value) { return _this._updateLatestValue(obj, value); }, error: (e: any) => { throw e; } }); } private _dispose() { this._subscription.unsubscribe(); this._internalSubscription.unsubscribe(); this._internalSubscription = null; this._latestValue = null; this._latestReturnedValue = null; this._subscription = null; this._obj = null; } private _updateLatestValue(async: any, value: Object) { if (async === this._obj) { this._latestValue = value; this._ref.markForCheck(); } } } |
You will notice that this pipe is not pure. But the main code is in internalTransform() . We will check if you ask for a different Url, if not, we will not make multiple GET request to your server.
We also use the DomSanitizer because Angular will complain that the blob scheme is unsafe.
Register Services and Pipes
Yep, don’t forget to do that…
The HTML
Simply use the secure pipe like this
1 |
<img [src]="'./path/request/private-photo?id=1234' | secure" alt="" /> |
Make sure to use the [src] since you are not actually returning a string but a “ SafeUrl “. Your image is now requested through your UrlHelper service with an access token!